diff options
| author | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
| commit | 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch) | |
| tree | b9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/app/(main)/links/[linkId] | |
| download | umami-main.tar.xz umami-main.zip | |
Created from https://vercel.com/new
Diffstat (limited to 'src/app/(main)/links/[linkId]')
| -rw-r--r-- | src/app/(main)/links/[linkId]/LinkControls.tsx | 32 | ||||
| -rw-r--r-- | src/app/(main)/links/[linkId]/LinkHeader.tsx | 19 | ||||
| -rw-r--r-- | src/app/(main)/links/[linkId]/LinkMetricsBar.tsx | 70 | ||||
| -rw-r--r-- | src/app/(main)/links/[linkId]/LinkPage.tsx | 34 | ||||
| -rw-r--r-- | src/app/(main)/links/[linkId]/LinkPanels.tsx | 83 | ||||
| -rw-r--r-- | src/app/(main)/links/[linkId]/page.tsx | 12 |
6 files changed, 250 insertions, 0 deletions
diff --git a/src/app/(main)/links/[linkId]/LinkControls.tsx b/src/app/(main)/links/[linkId]/LinkControls.tsx new file mode 100644 index 0000000..1d1147a --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkControls.tsx @@ -0,0 +1,32 @@ +import { Column, Row } from '@umami/react-zen'; +import { ExportButton } from '@/components/input/ExportButton'; +import { FilterBar } from '@/components/input/FilterBar'; +import { MonthFilter } from '@/components/input/MonthFilter'; +import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; +import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton'; + +export function LinkControls({ + linkId: websiteId, + allowFilter = true, + allowDateFilter = true, + allowMonthFilter, + allowDownload = false, +}: { + linkId: string; + allowFilter?: boolean; + allowDateFilter?: boolean; + allowMonthFilter?: boolean; + allowDownload?: boolean; +}) { + return ( + <Column gap> + <Row alignItems="center" justifyContent="space-between" gap="3"> + {allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />} + {allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />} + {allowDownload && <ExportButton websiteId={websiteId} />} + {allowMonthFilter && <MonthFilter />} + </Row> + {allowFilter && <FilterBar websiteId={websiteId} />} + </Column> + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkHeader.tsx b/src/app/(main)/links/[linkId]/LinkHeader.tsx new file mode 100644 index 0000000..a84a626 --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkHeader.tsx @@ -0,0 +1,19 @@ +import { IconLabel } from '@umami/react-zen'; +import { LinkButton } from '@/components/common/LinkButton'; +import { PageHeader } from '@/components/common/PageHeader'; +import { useLink, useMessages, useSlug } from '@/components/hooks'; +import { ExternalLink, Link } from '@/components/icons'; + +export function LinkHeader() { + const { formatMessage, labels } = useMessages(); + const { getSlugUrl } = useSlug('link'); + const link = useLink(); + + return ( + <PageHeader title={link.name} description={link.url} icon={<Link />}> + <LinkButton href={getSlugUrl(link.slug)} target="_blank" prefetch={false} asAnchor> + <IconLabel icon={<ExternalLink />} label={formatMessage(labels.view)} /> + </LinkButton> + </PageHeader> + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx new file mode 100644 index 0000000..1fe8c45 --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx @@ -0,0 +1,70 @@ +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useDateRange, useMessages } from '@/components/hooks'; +import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery'; +import { MetricCard } from '@/components/metrics/MetricCard'; +import { MetricsBar } from '@/components/metrics/MetricsBar'; +import { formatLongNumber } from '@/lib/format'; + +export function LinkMetricsBar({ + linkId, +}: { + linkId: string; + showChange?: boolean; + compareMode?: boolean; +}) { + const { isAllTime } = useDateRange(); + const { formatMessage, labels } = useMessages(); + const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId); + + const { pageviews, visitors, visits, comparison } = data || {}; + + const metrics = data + ? [ + { + value: visitors, + label: formatMessage(labels.visitors), + change: visitors - comparison.visitors, + formatValue: formatLongNumber, + }, + { + value: visits, + label: formatMessage(labels.visits), + change: visits - comparison.visits, + formatValue: formatLongNumber, + }, + { + value: pageviews, + label: formatMessage(labels.views), + change: pageviews - comparison.pageviews, + formatValue: formatLongNumber, + }, + ] + : null; + + return ( + <LoadingPanel + data={metrics} + isLoading={isLoading} + isFetching={isFetching} + error={error} + minHeight="136px" + > + <MetricsBar> + {metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => { + return ( + <MetricCard + key={label} + value={value} + previousValue={prev} + label={label} + change={change} + formatValue={formatValue} + reverseColors={reverseColors} + showChange={!isAllTime} + /> + ); + })} + </MetricsBar> + </LoadingPanel> + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx new file mode 100644 index 0000000..ddacf08 --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkPage.tsx @@ -0,0 +1,34 @@ +'use client'; +import { Column, Grid } from '@umami/react-zen'; +import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls'; +import { LinkHeader } from '@/app/(main)/links/[linkId]/LinkHeader'; +import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar'; +import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels'; +import { LinkProvider } from '@/app/(main)/links/LinkProvider'; +import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal'; +import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; +import { PageBody } from '@/components/common/PageBody'; +import { Panel } from '@/components/common/Panel'; + +const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event']; + +export function LinkPage({ linkId }: { linkId: string }) { + return ( + <LinkProvider linkId={linkId}> + <Grid width="100%" height="100%"> + <Column margin="2"> + <PageBody gap> + <LinkHeader /> + <LinkControls linkId={linkId} /> + <LinkMetricsBar linkId={linkId} showChange={true} /> + <Panel> + <WebsiteChart websiteId={linkId} /> + </Panel> + <LinkPanels linkId={linkId} /> + </PageBody> + <ExpandedViewModal websiteId={linkId} excludedIds={excludedIds} /> + </Column> + </Grid> + </LinkProvider> + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkPanels.tsx b/src/app/(main)/links/[linkId]/LinkPanels.tsx new file mode 100644 index 0000000..f33525e --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkPanels.tsx @@ -0,0 +1,83 @@ +import { Grid, Heading, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen'; +import { GridRow } from '@/components/common/GridRow'; +import { Panel } from '@/components/common/Panel'; +import { useMessages } from '@/components/hooks'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; +import { WorldMap } from '@/components/metrics/WorldMap'; + +export function LinkPanels({ linkId }: { linkId: string }) { + const { formatMessage, labels } = useMessages(); + const tableProps = { + websiteId: linkId, + limit: 10, + allowDownload: false, + showMore: true, + metric: formatMessage(labels.visitors), + }; + const rowProps = { minHeight: 570 }; + + return ( + <Grid gap="3"> + <GridRow layout="two" {...rowProps}> + <Panel> + <Heading size="2">{formatMessage(labels.sources)}</Heading> + <Tabs> + <TabList> + <Tab id="referrer">{formatMessage(labels.referrers)}</Tab> + <Tab id="channel">{formatMessage(labels.channels)}</Tab> + </TabList> + <TabPanel id="referrer"> + <MetricsTable type="referrer" title={formatMessage(labels.domain)} {...tableProps} /> + </TabPanel> + <TabPanel id="channel"> + <MetricsTable type="channel" title={formatMessage(labels.type)} {...tableProps} /> + </TabPanel> + </Tabs> + </Panel> + <Panel> + <Heading size="2">{formatMessage(labels.environment)}</Heading> + <Tabs> + <TabList> + <Tab id="browser">{formatMessage(labels.browsers)}</Tab> + <Tab id="os">{formatMessage(labels.os)}</Tab> + <Tab id="device">{formatMessage(labels.devices)}</Tab> + </TabList> + <TabPanel id="browser"> + <MetricsTable type="browser" title={formatMessage(labels.browser)} {...tableProps} /> + </TabPanel> + <TabPanel id="os"> + <MetricsTable type="os" title={formatMessage(labels.os)} {...tableProps} /> + </TabPanel> + <TabPanel id="device"> + <MetricsTable type="device" title={formatMessage(labels.device)} {...tableProps} /> + </TabPanel> + </Tabs> + </Panel> + </GridRow> + <GridRow layout="two" {...rowProps}> + <Panel padding="0"> + <WorldMap websiteId={linkId} /> + </Panel> + <Panel> + <Heading size="2">{formatMessage(labels.location)}</Heading> + <Tabs> + <TabList> + <Tab id="country">{formatMessage(labels.countries)}</Tab> + <Tab id="region">{formatMessage(labels.regions)}</Tab> + <Tab id="city">{formatMessage(labels.cities)}</Tab> + </TabList> + <TabPanel id="country"> + <MetricsTable type="country" title={formatMessage(labels.country)} {...tableProps} /> + </TabPanel> + <TabPanel id="region"> + <MetricsTable type="region" title={formatMessage(labels.region)} {...tableProps} /> + </TabPanel> + <TabPanel id="city"> + <MetricsTable type="city" title={formatMessage(labels.city)} {...tableProps} /> + </TabPanel> + </Tabs> + </Panel> + </GridRow> + </Grid> + ); +} diff --git a/src/app/(main)/links/[linkId]/page.tsx b/src/app/(main)/links/[linkId]/page.tsx new file mode 100644 index 0000000..4317ada --- /dev/null +++ b/src/app/(main)/links/[linkId]/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { LinkPage } from './LinkPage'; + +export default async function ({ params }: { params: Promise<{ linkId: string }> }) { + const { linkId } = await params; + + return <LinkPage linkId={linkId} />; +} + +export const metadata: Metadata = { + title: 'Link', +}; |